在插件化中,hook Activity作为最基本的技术,用来在宿主app中新增Activity,而通常情况下,Activity必须在Manifest中注册在才可以使用,下面将就Android10.0来分析hook Activity的详细过程。
要hook Activity之前,必须知道Activity的启动过程,才能够选择合适的点进行hook,在前面的文章中有分析Activity详细的启动过程,hook主要是两个点:一是在Activity给AMS之前替换代理的Activity,二是在handler中发送启动Activity时替换为插件的Activity。
分析完了Activity的hook点之后,还有一个重要的问题,如何加载插件的类和资源,只有加载了插件中的类和资源,后面的hook才有意义。
一、类加载
在Android中,将代码编译后会生成apk文件,apk文件里面有一个或多个classes.dex文件,它是所有class文件进行合并,优化后生成。在apk运行时ART虚拟机或Dalvik虚拟机会加载dex文件,加载都是通过ClassLoader实现。
ClassLoader是一个抽象类,实现分为系统类加载器和自定义类加载器。
系统类加载器有三种:
BootClassLoader:用于加载Android Framework中class文件。
PathClassLoader:用于Android应用程序类加载器,可以加载指定的dex,以及jar、zip、apk中的dex。
DexClassLoader:用于加载指定的dex,以及jar、zip、apk中的dex。
1.1 PathClassLoader
[->libcore\dalvik\src\main\java\dalvik\system\PathClassLoader.java]
1 | public class PathClassLoader extends BaseDexClassLoader { |
BaseDexClassLoader
[->libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java]
1 | public class BaseDexClassLoader extends ClassLoader { |
1.2 DexClassLoader
[->libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java]
1 | public class DexClassLoader extends BaseDexClassLoader { |
PathClassLoader和DexClassLoader两者都是继承于BaseDexClassLoader,并且类中只有构成方法,实现全部在BaseDexClassLoader中。从源码中可以看出DexClassLoader多个一个optimizedDirectory参数,但是实际上没什么用处,这两者最后调用的super方法一模一样。
1.3 加载原理
类加载器通过loadClass方法加载apk文件中的类。
1.3.1 ClassLoader.loadClass
[->libcore\ojluni\src\main\java\java\lang\ClassLoader.java]
1 | protected Class<?> loadClass(String name, boolean resolve) |
上面类加载的过程就是双亲委派机制。先检查类是否已经被加载,如果已经加载,直接获取并返回。如果没有被加载,parent不为空,则调用parent的loadClass进行加载,依次递归,直到找到或者加载了就返回;如果还没有找到也加载不了,则自己去加载。
BootClassLoader是最后一个加载器,BootClassLoader重写了findClass和loadClass方法,并且在loadClass方法中不再获取parent,从而结束递归。
在所有parent都不能加载的情况下,DexClassLoader加载过程如下,它的父类继承了BaseDexClassLoader,并重写了findClass方法。
1.3.2 BaseDexClassLoader.findClass
[->libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java]
1 | @Override |
1.3.3 DexPathList.findClass
[->\libcore\dalvik\src\main\java\dalvik\system\DexPathList.java]
1 | /** |
dexElements初始化在构造函数中完成
1 | DexPathList(ClassLoader definingContext, String dexPath, |
生成Element数组,每一个dex对应一个Element
1 | private static Element[] makeDexElements(List<File> files, File optimizedDirectory, |
Class对象从Element中获取,每一个Element对应一个dex文件。通过上面的分析,可以想到插件apk的加载方法,通过DexClassLoader加载器加载插件apk,再通过反射的方法获取到宿主的dexElements,最后将插件dexElements和宿主的dexElements合并并赋值给dexElements,其详细流程如下:
1.创建插件的DexClassLoader类加载器,通过反射获取插件中的dexElements
2.获取宿主的PathClassLoader类加载器,通过反射获取宿主的dexElements
3.合并插件和宿主的dexElements,生成新的Element[]
4.最后通过反射将新的Element[]赋值给宿主的dexElements
1 | //获取BaseDexClassLoader类的class |
这样就完成了插件apk中类的加载,热修复也用到了这样的类加载的原理,不同的是将插件的Elements放在了最前面。
二、资源加载
要加载插件中的资源,可以通过AssetManager来实现,因为资源的加载实际上是通过AssetManager来加载的。AssetManager可以通过文件名访问那些被编译过的应用程序的资源文件也可以访问没有被编译过的应用程序资源文件。
下面分析下AssetManager如何加载资源的,获取资源是通过getResources方法:
在ActivityThread中attach 方法里面的createAppContext创建Context时会设置Resources。
2.1 CL.createAppContext
[->base\core\java\android\app\ContextImpl.java]
1 | static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) { |
2.2 LoadedApk.getResources
[->base\core\java\android\app\LoadedApk.java]
1 | public Resources getResources() { |
2.3 RM.getResources
[->base\core\java\android\app\ResourcesManager.java]
1 | public @Nullable Resources getResources(@Nullable IBinder activityToken, |
2.4 RM.getOrCreateResources
[->base\core\java\android\app\ResourcesManager.java]
1 | private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken, |
2.5 RM.createResourcesImpl
[->base\core\java\android\app\ResourcesManager.java]
1 | private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) { |
2.6 RM.createAssetManager
[->base\core\java\android\app\ResourcesManager.java]
1 | protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) { |
2.7 AssetManager.addApkAssets
[->base\core\java\android\content\res\AssetManager.java]
1 | public Builder addApkAssets(ApkAssets apkAssets) { |
2.8 RM.loadApkAssets
[->base\core\java\android\app\ResourcesManager.java]
1 | private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay) |
按照这样的流程比较难hook,看下之前添加的方法addAssetPath,这个方法已经被废弃,但还可以用,hook这个方法比较简单。
1 | /** |
1.创建一个AssetManager对象,并调用addAssetPath方法,将插件apk路径作为参数传入
2.将创建的AssetManager对象作为参数,创建一个新的Resourced对象,并返回给插件使用。
1 | public static Resources loadResource(Context context){ |
1 | private Resources resources; |
然后让插件的Activity重写getResources方法,获取的资源就是新创建的resources对象
1 | @Override |
三、hook Activity
首先在宿主里面创建一个ProxyActivity,并且在Manifest中注册(如果需要对应不同启动模式的Activity,可以全部把每种启动模式下的Activity都注册)。当启动插件Activity时,进入AMS之前,通过Hook将插件Activity替换成ProxyActivity,在进入AMS之后,通过handler发送消息时使用hook将ProxyActivity替换成插件的Activity。
startActivity的流程如下图,可以看到这两个具体的hook点
3.1 hook AMS
在进入到AMS之前,这个是最后一步,那么如何将intent换成插件的intent的呢?可以通过动态代理实现。
1 | public ActivityResult execStartActivity( |
ActivityManager.getService具体实现如下
1 | /** |
第一步获取IActivityManager对象
1 | //获取IActivityManagerSingleton对象,用于获取mIntance |
第二步将intent替换成插件的intent
1 | Class<?> iActivityManagerClass = Class.forName("android.app.IActivityManager"); |
这样第一个hook点就完成了,下面看下第二步hook点
3.2 hook Handler
在启动Activity之前的操作,Android10.0用状态模式完成Activity的生命周期的启动,那么如何将代理的intent替换成插件的intent的呢?从源码可以看出最后通过scheduleTransaction方法启动Activity,那么是否通过clientTransaction替换intent的呢,通过分析startActivity的启动流程,答案是可以的。
1 | //创建Activity启动事务 |
scheduleTransaction还是要通过Handler发送消息,进入EXECUTE_TRANSACTION分支
1 | public void handleMessage(Message msg) { |
那么要替换intent首先需要hook handleMessage
1 | //获取ActivityThread对象 |
hook了handleMessage后,通过LaunchActivityItem中的mIntent完成代理intent的替换。
四、总结
hook Activity涉及的技术比较多,Activity的启动流程,类加载,动态代理,资源加载,反射,binder机制等,只有在掌握了这些技术的基础上才能够完成插件化技术的开发。
本文介绍了hook Activity的具体实现方法,通过该方法引申出类的加载和资源加载的原理,并分析具体插件的加载实现,后面结合之前文章分析的startActivity的启动流程,引出了两个具体的hook点:
1.在ActivityManager.getService().startActivity时通过反射获取IActivityManager对象,startActivity时将插件的Activity替换成代理的Activity;
2.反射获取ActivityThread对象,通过获取类成员变量mH,重新设置callback,将handleMessage中的EXECUTE_TRANSACTION分支进行hook,在mActivityCallbacks中找到LaunchActivityItem,将其类成员变量mIntent替换成插件的Activity,这样就完成的插件Activity的启动过程。